<?php
declare (strict_types=1);

namespace app\common\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Cache;
use think\facade\Db;

class Mirror extends Command
{
    protected $errno;
    protected $error;

    protected $originHost = 'https://wampserver.aviatechno.net/';

    protected function configure()
    {
        $this->setName('mirror')
            ->addArgument('argument', Argument::OPTIONAL, "更新镜像文件")
            ->addOption('skip-md5', null, Option::VALUE_NONE, '跳过MD5校验')
            ->setDescription('WampServer镜像文件');
    }

    protected function execute(Input $input, Output $output)
    {
        // 设置执行用户
        $this->getConsole()->setUser('www');

        // 更改工作目录
        chdir(app()->getRootPath());

        // 设置输出信息级别
        $output->setVerbosity(Output::VERBOSITY_VERBOSE);

        switch (trim((string)$input->getArgument('argument'))) {
            case 'check':
                $this->checkAction($input, $output);
                break;
            case 'sync':
                $this->syncAction($input, $output);
                break;
            default:
                $output->writeln('end');
        }
    }

    /**
     * 校验文件MD5和大小
     * @param \think\console\Input $input
     * @param \think\console\Output $output
     * @throws \think\db\exception\DbException
     */
    protected function checkAction(Input $input, Output $output)
    {
        $output->writeln('开始校验文件md5和文件大小...');

        $update_num = 0;
        $not_exists_num = 0;
        $skip_num = 0;

        $result = Db::table('software')->cursor();
        foreach ($result as $res) {
            $file = 'public/' . $res['url'];
            if (file_exists($file)) {
                $data = [];

                $md5 = md5_file($file);
                if ('' == $res['md5']) {
                    $data['md5'] = $md5;
                    $output->writeln($res['url'] . ':数据库无记录MD5');
                } elseif ($md5 != $res['md5']) {
                    $output->writeln($res['url'] . ':文件MD5与数据库记录不一致');
                    $output->writeln('数据库MD5:' . $res['md5']);
                    $output->writeln('本地文件MD5:' . $md5);
                    $output->writeln('尝试重新下载远程文件');
                    // 远程地址
                    $remoteUrl = $this->originHost . $res['url'];
                    // 本地地址
                    $localPath = 'runtime/wampserver/md5_update.tmp';
                    // 下载文件
                    $download = $this->download($remoteUrl, $localPath, function($current, $total, $speed) use ($output) {
                        // 当前进度(百分比)
                        $percent = round($current * 100 / $total, 2);
                        $output->write($this->progressBar($percent, null, round($speed / 1024, 2) . 'KB/s'));
                    });

                    if (!$download) {
                        $output->writeln('文件下载失败[' . $this->errno . ']:' . $this->error);
                    } else {
                        $md5 = md5_file($localPath);
                        if ($md5 != $res['md5']) {
                            $output->writeln('远程文件MD5:' . $md5);
                            $output->writeln('远程文件MD5与数据库MD5校验不通过');
                        } else {
                            // 重命名旧文件
                            rename($file, $file . date('.Ymd'));
                            // 移动远程文件
                            rename($localPath, $file);
                        }
                    }
                } else {
                    $skip_num++;
                }

                $fileseze = filesize($file);
                if ($fileseze != $res['size']) {
                    $data['size'] = $fileseze;
                    $output->writeln($res['url'] . ':文件大小与数据库记录不一致');
                    $output->writeln('数据库:' . $res['size']);
                    $output->writeln('本地文件:' . $fileseze);
                }

                if (count($data) > 0) {
                    $update_num++;
                    Db::table('software')->where('id', $res['id'])->update($data);
                }
            } else {
                $not_exists_num++;
                $output->writeln($res['url'] . ':文件不存在');
            }
        }

        $output->writeln( '更新:' . $update_num . '; 文件不存在:' . $not_exists_num . '; 跳过:' . $skip_num);;
    }

    /**
     * 同步远程站点文件
     * @param \think\console\Input $input
     * @param \think\console\Output $output
     */
    protected function syncAction(Input $input, Output $output)
    {
        $output->writeln('获取镜像源所有文件URL...');

        $urls = $this->mirrorURLs();
        if (false === $urls) {
            $output->writeln('发生错误，错误代码：' . $this->errno . '，错误信息：' . $this->error);
            return;
        }

        // 跳过MD5校验
        $skipMd5 = $input->hasOption('skip-md5');

        $output->writeln('获取成功，共' . count($urls) . '条URL!');
        $output->writeln('开始判断资源是否有更新...');
        foreach ($urls as $url) {
            // MD5
            $url['md5'] = strtolower($url['md5']);
            // 远程地址
            $remoteUrl = $this->originHost . $url['url'];
            // 本地地址
            $localPath = 'public/' . $url['url'];

            // 查找本地记录
            $info = Db::table('software')->where('type', $url['type'])->where('bit', $url['bit'])->where('version', $url['version'])->find();
            if (null === $info) {
                $output->writeln('[add]' . $url['name']);
            } elseif (1 === $info['discard']) {
                $output->writeln('[discard]' . $url['name']);
                continue;
            } elseif (false === $skipMd5 && $info['md5'] != $url['md5']) {
                $output->writeln('[update]' . $url['name']);
            } else {
                $output->writeln('[ignore]' . $url['name']);
                if (file_exists($localPath)) continue;
            }

            // 下载文件
            $download = $this->download($remoteUrl, $localPath, function($current, $total, $speed) use ($output) {
                // 当前进度(百分比)
                $percent = round($current * 100 / $total, 2);
                $output->write($this->progressBar($percent, null, round($speed / 1024, 2) . 'KB/s'));
            });

            if (!$download) {
                $output->writeln('文件下载失败，错误代码：' . $this->errno . '，错误信息：' . $this->error);
                if (null === $info) unlink($localPath);
                continue;
            }

            if (false === $skipMd5) {
                $md5 = md5_file($localPath);
                if ($md5 != $url['md5']) {
                    $output->writeln('MD5校验不通过，' . $md5 . '!=' . $url['md5']);
                    if (null === $info) unlink($localPath);
                    continue;
                }
            }

            $url['size'] = filesize($localPath);

            if (null === $info) {
                $url['create_time'] = date('Y-m-d H:i:s');
                $url['update_time'] = date('Y-m-d H:i:s');
                Db::table('software')->insert($url);
            } else {
                $url['update_time'] = date('Y-m-d H:i:s');
                Db::table('software')->where('id', $info['id'])->update($url);
            }
        }
    }

    /**
     * 下载文件
     * @param string $url 远程地址
     * @param string $filename 本地地址
     * @param callable $progress 显示进度
     * @return bool
     */
    protected function download($url, $filename, callable $progress = null)
    {
        $this->mkdir($filename);

        $handle = fopen($filename, 'w');
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
        curl_setopt($ch, CURLOPT_PROXY, "127.0.0.1:51080");
        curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$handle, $progress) {
            static $size;
            static $total;

            if (null === $size) {
                $size = 0;
                $total = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
            }

            $length =  fwrite($handle, $data);
            $size += $length;

            if (null !== $progress) {
                $progress($size, $total, curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD));
            }

            return $length;
        });
        curl_exec($ch);
        fclose($handle);

        if (curl_errno($ch)) {
            $this->error = curl_error($ch);
            $this->errno = curl_errno($ch);

            curl_close($ch);

            return false;
        } else {
            curl_close($ch);

            return true;
        }
    }

    /**
     * 获取镜像源所有资源地址
     * @return array|bool
     */
    protected function mirrorURLs()
    {
        $cache_key = 'wampserver.aviatechno.net';

        $data = Cache::get($cache_key);
        if (null === $data) {
            $ch = curl_init($this->originHost);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HEADER, false);
            curl_setopt($ch, CURLOPT_AUTOREFERER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_TIMEOUT, 60);
            curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
            curl_setopt($ch, CURLOPT_PROXY, "127.0.0.1:51080");
            curl_setopt($ch, CURLOPT_POSTFIELDS, [
                'lang'        => 'en',
                'allupdateversions'  => 'afficher',
                'allappsversions'    => 'afficher',
                'allphpversions'     => 'afficher',
                'allmysqlversions'   => 'afficher',
                'allmariadbversions' => 'afficher',
                'allvcpackversions'  => 'afficher',
                'allapacheversions'  => 'afficher',
            ]);

            $data = curl_exec($ch);

            if (curl_errno($ch)) {
                $this->error = curl_error($ch);
                $this->errno = curl_errno($ch);

                curl_close($ch);

                return false;
            } else {
                curl_close($ch);

                Cache::set($cache_key, $data, 86400);
            }
        }

        if (preg_match_all('@href=\'(files/[\w\-_./]+)\'>(.*)</a>.*md5popup\'>(\w+)</@', $data, $matches)) {
            [$text, $url, $name, $md5] = $matches;

            $result = [];
            foreach ($url as $key => $value) {
                if (0 === strpos($name[$key], 'Update xDebug')) {
                    $table = 'xdebug';
                } elseif (0 === strpos($name[$key], 'Phpmyadmin')) {
                    $table = 'phpmyadmin';
                } elseif (0 === strpos($name[$key], 'Phpsysinfo')) {
                    $table = 'phpsysinfo';
                } elseif (0 === strpos($name[$key], 'Adminer')) {
                    $table = 'adminer';
                } elseif (0 === strpos($name[$key], 'VC')) {
                    if (strpos($name[$key], 'ATL')) continue;
                    if (strpos($name[$key], 'MFC')) continue;
                    $table = 'vcredist';
                } elseif (0 === strpos($name[$key], 'Apache')) {
                    $table = 'apache';
                } elseif (0 === strpos($name[$key], 'PHP')) {
                    $table = 'php';
                } elseif (0 === strpos($name[$key], 'MariaDB')) {
                    $table = 'mariadb';
                } elseif (0 === strpos($name[$key], 'MySQL')) {
                    $table = 'mysql';
                } elseif (0 === strpos($name[$key], 'wampmanager.ini')) {
                    $table = 'wampmanager.ini';
                } elseif (0 === strpos($name[$key], 'Checks VC++')) {
                    $table = 'check_vcredist';
                } elseif (0 === strpos($name[$key], 'All VC')) {
                    $table = 'all_vcredist';
                } elseif (0 === strpos($name[$key], 'Update Wampserver') || 0 === strpos($name[$key], 'Wampserver update')) {
                    $table = 'update_wampserver';
                } elseif (0 === strpos($name[$key], 'Wampserver')) {
                    $table = 'wampserver';
                } elseif (0 === strpos($name[$key], 'Aestran Tray Menu')) {
                    $table = 'wampmanager';
                } elseif (0 === strpos($name[$key], 'Language files')) {
                    $table = 'language';
                } else {
                    halt($name[$key]);
                    continue;
                }

                if (false !== strpos($value, '_x86_x64')) {
                    $bit = 0;
                } elseif (false !== strpos($value, '_x86')) {
                    $bit = 32;
                } elseif (false !== strpos($value, '_x64')) {
                    $bit = 64;
                } else {
                    $bit = 0;
                }

                if (preg_match('@(\d+)(\.[0-9abcd]+){2,3}@u', $value, $matches)) {
                    $version = $matches[0];
                } elseif (preg_match('@(\d{4})@', $value, $matches)) {
                    $version = $matches[0];
                } else {
                    $version = '';
                }

                $result[] = [
                    'name'    => $name[$key],
                    'url'     => $url[$key],
                    'md5'     => $md5[$key],
                    'version' => $version,
                    'bit'     => $bit,
                    'type'    => $table,
                ];
            }

            return $result;
        } else {
            $this->errno = 1;
            $this->error = 'match urls failed';

            return false;
        }
    }

    /**
     * 根据文件路径自动创建目录
     * @param string $path 文件路径
     */
    protected function mkdir($path)
    {
        $list = explode('/', $path);
        $path = '';
        foreach ($list as $dir) {
            if (false !== strpos($dir, '.')) continue;

            $path .= $dir . '/';
            if (!is_dir($path)) mkdir($path);
        }
    }

    /**
     * 进度条
     * @param float $progress 进度0-100
     * @param string $left 左进度信息
     * @param string $right 右进度信息
     * @return string
     */
    protected function progressBar($progress, $left = null, $right = '')
    {
        // 左进度信息
        if (null === $left) $left = number_format($progress, 2) . '%';

        // 获取终端行字符长度
        if (0 === stripos(PHP_OS, 'Win')) {
            // Windows系统
            if (preg_match('@Columns:\s+(\d+)@', `mode`, $matches)) {
                $terminalLength = $matches[1];
            } else {
                $terminalLength = 50;
            }
        } else {
            $terminalLength = trim(`tput cols`);
        }
        $terminalLength = intval($terminalLength);

        // 左进度信息长度
        $leftLength = strlen($left);
        // 右进度信息长度
        $rightLength = strlen($right);
        // 进度条长度
        $barLength = $terminalLength - $leftLength - $rightLength - 2;
        // 已完成进度条长度
        $completeBarLength = (int)round($progress / 100 * $barLength);
        // 空进度条长度
        $emptyBarLength = $barLength - $completeBarLength;

        // 当进度条走完之后，
        if (100 == $progress) $right .= PHP_EOL;

        return sprintf(
            "%s[%s%s]%s\r",
            $left,
            str_repeat('=', $completeBarLength),
            str_repeat(' ', $emptyBarLength),
            $right
        );
    }
}
